package main.model.builder;

import java.text.ParseException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import main.controller.GEDIVAController;
import main.controller.MVCEvent;
import main.controller.MVCEvent.EventType;
import main.controller.MVCObservable;
import main.model.DataModel;
import main.model.NullModel;
import main.model.parser.CSVParseStrategy;
import main.model.parser.NullParseStrategy;
import main.model.parser.ParseStrategy;
import main.model.parser.TabParseStrategy;


/**
 * Builder abstract class for generic models.
 *
 * @author Kevin Gao
 *
 */
public abstract class ModelBuilder extends MVCObservable {

    private ParseStrategy myParsingStrategy;
    private List<ParseStrategy> myPossibleStrategies;
    private DataModel myModel = new NullModel();
    private String myFileType;
    private final GEDIVAController myController;

    /**
     * Default constructor that loads possible parsing strategies.
     *
     * @param controller The controller to register events with
     * @param fileType file type to use
     */
    public ModelBuilder (GEDIVAController controller, String fileType) {
        super(controller);
        myController = controller;
        myFileType = fileType;
        populateStrategies();
        chooseParsingStrategy();
    }

    /**
     * Constructor without knowing filetype.
     *
     * @param controller Associated controller
     */
    public ModelBuilder (GEDIVAController controller) {
        super(controller);
        myController = controller;
        populateStrategies();
    }

    /**
     * After construction, new lines can be passed in order to continue
     * building.
     *
     * @param line line to read in unparsed format
     */
    public void build (String line) {
        try {
            buildFromParsed(myParsingStrategy.parseLineToRow(line));
        }
        // Check if parseLineToRow throws
        catch (ParseException e) {
            notifyObserversImmediately(new MVCEvent(EventType.ERROR, e));
        }
    }

    /**
     * After parsing a line to an ADT actually build something out of it.
     *
     * @param data
     * @throws ParseException
     */
    protected abstract void buildFromParsed (Map<String, Object> data) throws ParseException;

    /**
     * Sets the headers for all of builder's parsers.
     *
     * @param line first row of data file w/ headers
     */
    public void setHeaders (String line) {
        for (ParseStrategy ps : myPossibleStrategies) {
            ps.setHeaders(line);
        }
    }

    /**
     * Creates a new model to work with. Should usually be used by the
     * constructor.
     *
     * @param model Should be a newly instantiated model
     */
    protected void setModel (DataModel model) {
        myModel = model;
    }

    /**
     * Returns the built (thus far) model.
     *
     * @return
     */
    public DataModel toModel () {
        if (myModel instanceof NullModel) {
            notifyObserversImmediately(new MVCEvent(EventType.ERROR,
                                                    "Model was never set up correctly."));
        }
        return myModel;
    }

    /**
     * Template method that resets the builder for in preparation for a new
     * build.
     */
    public void flush () {
        resetModel();
        clearHeaders();
        // Register controller as an observer for all models that are built
        myModel.addObserver(myController);
    }

    /**
     * Resets the internal model so that a new model can be built upon.
     */
    protected abstract void resetModel ();

    /**
     * Clears headers.
     */
    private void clearHeaders () {
        for (ParseStrategy ps : myPossibleStrategies) {
            ps.clearHeaders();
        }
    }

    /**
     * @return the myController
     */
    public GEDIVAController getController () {
        return myController;
    }

    /**
     * Sets a new file type and changes the parsing strategy used.
     *
     * @param fileType file type to change to
     */
    public boolean setFileType (String fileType) {
        myFileType = fileType;
        return chooseParsingStrategy();
    }

    private boolean chooseParsingStrategy () {
        for (ParseStrategy ps : myPossibleStrategies) {
            if (ps.canRead(myFileType)) {
                myParsingStrategy = ps;
                return true;
            }
        }

        myParsingStrategy = new NullParseStrategy();
        notifyObserversImmediately(new MVCEvent(EventType.ERROR,
                                                "Could not find a parser for file type: " +
                                                        myFileType));
        return false;
    }

    private void populateStrategies () {
        myPossibleStrategies = new ArrayList<ParseStrategy>();

        // ---------- START PARSER INSTANTIATIONS ----------//
        myPossibleStrategies.add(new CSVParseStrategy());
        myPossibleStrategies.add(new TabParseStrategy());
        // ----------- END PARSER INSTANTIATIONS -----------//
    }

    /**
     * A method to be implemented for each builder that determines whether or
     * not it can build a certain type, denoted by a string.
     *
     * @param type Type as a string
     * @return
     */
    public abstract boolean canBuild (String type);

    /**
     * Sets the name for the current model.
     *
     * @param name Name to set
     */
    public void setIdentifier (String name) {
        myModel.setName(name);
    }

}
